/*
 * Copyright (c) 2010-2013 Ruben Chakhmakhchyan. All rights reserved.
 * 
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Eclipse Public License
 * Version 1.0 (the 'License'). You may not use this file except in
 * compliance with the License. Please obtain a copy of the License at
 * http://opensource.org/licenses/EPL-1.0 and read it before using this file.
 */

#ifdef XP_WIN

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

#include <windows.h>
#include <windowsx.h>
#include <Dbt.h>

#endif

#ifdef XP_MAC
#include <TextEdit.h>
#endif

#ifdef XP_UNIX
#include <string.h>
#endif

#include "plugin_impl.h"
#include "strings.h"
#include "resource.h"

struct NPIds npid = { 0 };


// global plugin variables

static bool g_isInitialized = false;
static void* g_ptr = NULL;


_Plugin::_Plugin( NPP npp )
    : m_npp( npp ),
    m_isInitialized( false ),
    m_scriptableObject( NULL ),
    m_model( NULL ),
#ifdef XP_WIN
    m_hWnd( NULL ),
#endif
    m_normalCursor( NULL ),
    m_zoomCursor( NULL ),
    m_panCursor( NULL ),
    m_cropCursor( NULL )
{
    ::NPN_GetValue( m_npp, NPNVWindowNPObject, &m_npoWindow );

    if ( !g_isInitialized )
    {
#define NPSID( id ) npid.##id = ::NPN_GetStringIdentifier( #id )

        NPSID( scan );
#ifndef _ADDON
        NPSID( upload );
#endif
        NPSID( saveToFile );
        NPSID( saveToArray );
        NPSID( zoomTool );
        NPSID( zoom );
        NPSID( zoomIn );
        NPSID( zoomOut );
        NPSID( zoomReset );
        NPSID( fitZoom );
        NPSID( panTool );
        NPSID( pan );
        NPSID( panX );
        NPSID( panY );
        NPSID( panOn );
        NPSID( panOff );
        NPSID( rotate );
        NPSID( cropTool );
        NPSID( crop );
        NPSID( documentCookie );
        NPSID( onScanComplete );
        NPSID( onImageAcquired );
        NPSID( onSelectedImageChanged );
        NPSID( onImageCountChanged );
        NPSID( onZoomChanged );
#ifndef _ADDON
        NPSID( onUploadComplete );
        NPSID( onUploadFailed );
#endif
        NPSID( onDeviceEvent );
        NPSID( onToolStateChanged );
        NPSID( onError );
        NPSID( pixelType );
        NPSID( resolution );
        NPSID( transferMode );
        NPSID( getDevices );
        NPSID( devices );
        NPSID( deviceIndex );
        NPSID( deviceType );
        NPSID( productName );
        NPSID( id );
        NPSID( uid );
        NPSID( index );
        NPSID( name );
        NPSID( manufacturer );
        NPSID( testMethod );
        NPSID( testProperty );
        NPSID( selectedDeviceId );
        NPSID( driverModel );
        NPSID( showDeviceUI );
        NPSID( selectedIndex );
        NPSID( selectImage );
        NPSID( selectNextImage );
        NPSID( selectPreviousImage );
        NPSID( paperSource );
        NPSID( cap );
        NPSID( type );
        NPSID( valueType );
        NPSID( count );
        NPSID( value );
        NPSID( item );
        NPSID( values );
        NPSID( items );
        NPSID( min );
        NPSID( max );
        NPSID( step );
        NPSID( defaultValue );
        NPSID( currentValue );
        NPSID( defaultIndex );
        NPSID( currentIndex );
        NPSID( getCap );
        NPSID( connect );
        NPSID( disconnect );
        NPSID( images );
        NPSID( width );
        NPSID( height );
        NPSID( resolution );
        NPSID( pixelType );
        NPSID( at );
        NPSID( get );
        NPSID( getThumbnailUrl );
        NPSID( clear );
        NPSID( imageQuality );
        NPSID( contrast );
        NPSID( version );
        NPSID( getFilter );
        NPSID( setFilter );
        NPSID( autoPosition );
        NPSID( autoCompress );
        NPSID( resume );
        NPSID( autoPause );
        NPSID( pause );
        NPSID( autoCompressionLevel );
        NPSID( autoCompressInBackground );
        NPSID( thumbnailSize );
        NPSID( initializeDevice );
        NPSID( isFlatbedSupported );
        NPSID( isFeederSupported );
        NPSID( isDuplexSupported );
        NPSID( pageWidth );
        NPSID( pageHeight );
        NPSID( isConnected );
        NPSID( orientation );
        NPSID( onSaveComplete );
        NPSID( isPlatformSupported );
        NPSID( getFolder );
        NPSID( openFolder );
        NPSID( focus );
        NPSID( deleteImage );
        NPSID( shiftImage );
        NPSID( moveImage );
        NPSID( current );
        NPSID( getPixel );
        NPSID( setPixel );

        // dom identifiers
        NPSID( document );
        NPSID( location );
        NPSID( href );
        NPSID( push );
        NPSID( pop );
        NPSID( Array );
        NPSID( constructor );
        NPSID( length );
        NPSID( getImageData );
        //NPSID(  );

#undef NPSID

        g_isInitialized = true;
    }

    // set user agent
    const char *ua = ::NPN_UserAgent( m_npp );
    size_t len = String::GetLength( ua ) + 1;
    m_userAgent = new char[ len ];
    ::memcpy( m_userAgent, ua, len );

    // create model
    ::CreatePluginModel( this, this );

    /*HCURSOR hCursor = LoadCursor( GetModuleHandle( NULL ), MAKEINTRESOURCE( IDC_CURSOR1 ) );
    SetCursor( hCursor );*/
}

_Plugin::~_Plugin()
{
    close();

    if ( m_scriptableObject )
    {
        m_scriptableObject->m_pPlugin = NULL;
        ::NPN_ReleaseObject( m_scriptableObject );
        m_scriptableObject = NULL;
    }

    if ( m_npoWindow )
    {
        ::NPN_ReleaseObject( m_npoWindow );
        m_npoWindow = NULL;
    }

    if ( m_normalCursor )
    {
        ::DestroyCursor( m_normalCursor );
        ::DestroyCursor( m_zoomCursor );
        ::DestroyCursor( m_panCursor );
        ::DestroyCursor( m_cropCursor );
    }

    delete m_userAgent;

    delete m_model;
    m_model = NULL;
}

#ifdef XP_WIN
static LRESULT CALLBACK PluginWndProc( HWND, UINT, WPARAM, LPARAM );
static WNDPROC lpOldProc = NULL;
#endif

NPBool _Plugin::init( NPWindow* pNPWindow )
{
    if( pNPWindow == NULL )
        return false;

#ifdef XP_WIN
    m_hWnd = ( HWND ) pNPWindow->window;

    if( m_hWnd == NULL )
        return false;

    // subclass window so we can intercept window messages and do our drawing to it
    lpOldProc = SubclassWindow( m_hWnd, ( WNDPROC ) ::PluginWndProc );

    // associate window with our Plugin object so we can access it in the window procedure
    ::SetWindowLongPtr( m_hWnd, GWLP_USERDATA, ( LONG_PTR )this );
#endif

    m_npWindow = pNPWindow;

    m_model->resize( pNPWindow->width, pNPWindow->height );

    m_isInitialized = true;



    //---------------------------------------

    GUID guid1 = { 0xA5DCBF10, 0x6530, 0x11D2, { 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED } }; //GUID_DEVINTERFACE_USB_DEVICE
    GUID guid2 = { 0x53f56307, 0xb6bf, 0x11d0, { 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b } }; // GUID_DEVINTERFACE_DISK

    DEV_BROADCAST_DEVICEINTERFACE notificationFilter = { 0 };

    //::ZeroMemory( &notificationFilter, sizeof( NotificationFilter ) );
    notificationFilter.dbcc_size = sizeof( DEV_BROADCAST_DEVICEINTERFACE );
    notificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
    notificationFilter.dbcc_classguid = guid1;

    HDEVNOTIFY hDeviceNotify = RegisterDeviceNotification( 
        m_hWnd,						// events recipient
        &notificationFilter,		// type of device
        DEVICE_NOTIFY_WINDOW_HANDLE	// type of recipient handle
        );

    if ( hDeviceNotify == NULL ) 
    {
        //ErrorHandler( TEXT( "RegisterDeviceNotification" ) );
        return false;
    }

    return true;
}

void _Plugin::close()
{
#ifdef XP_WIN
    // subclass it back
    SubclassWindow( m_hWnd, lpOldProc );
    m_hWnd = NULL;
#endif

    m_isInitialized = false;
}

int16_t _Plugin::handleEvent( void* event )
{
#ifdef XP_MAC
    NPEvent* ev = ( NPEvent* )event;
    if ( m_Window )
    {
        Rect box = { m_Window->y, m_Window->x,
            m_Window->y + m_Window->height, m_Window->x + m_Window->width };
        if ( ev->what == updateEvt )
        {
            ::TETextBox( m_String, strlen( m_String ), &box, teJustCenter );
        }
    }
#endif
    return 0;
}

NPObject* _Plugin::createScriptableObject()
{
    if ( !m_scriptableObject )
    {
        if ( m_scriptableObject = ( ScriptablePluginObject* ) ::NPN_CreateObject( m_npp, GET_NPOBJECT_CLASS( ScriptablePluginObject ) ) )
        {
            m_scriptableObject->m_pPlugin = this;
            ::NPN_RetainObject( m_scriptableObject );
        }
        else
        {
            onError( ResultCode::GeneralFailure );
        }
    }
    else // added to check if the exception goes away
    {
        ::NPN_RetainObject( m_scriptableObject );
    }

    return m_scriptableObject;
}

bool _Plugin::setCursors()
{
    if ( !m_normalCursor )
    {
        HMODULE hModule = NULL;
        BOOL result = ::GetModuleHandleExW( GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, ( LPCWSTR ) &g_ptr, &hModule );

        m_zoomCursor = ( HCURSOR ) ::LoadImageW( hModule, MAKEINTRESOURCE( IDC_CURSOR1 ), IMAGE_CURSOR, 16, 16, 0 );
        m_panCursor = ( HCURSOR ) ::LoadImageW( hModule, MAKEINTRESOURCE( IDC_CURSOR2 ), IMAGE_CURSOR, 16, 16, 0 );
        m_cropCursor = ( HCURSOR ) ::LoadImageW( hModule, MAKEINTRESOURCE( IDC_CURSOR3 ), IMAGE_CURSOR, 16, 16, 0 );
        m_normalCursor = ::LoadCursorW( NULL, IDC_ARROW );
    }

    InteractiveTool tool = m_model->getCurrentTool();
    HCURSOR cursor = m_normalCursor;
    switch ( tool )
    {
    case InteractiveTool::Zoom:
        cursor = m_zoomCursor;
        break;

    case InteractiveTool::Pan:
        cursor = m_panCursor;
        break;

    case InteractiveTool::Crop:
        cursor = m_cropCursor;
        break;

    default:
        //cursor = m_normalCursor;
        break;
    }

    ::SetCursor( cursor );

    return true;
}

#ifndef _ADDON
void uploadCallback( AsyncInvokeArg* arg )
{
    arg->getPluginModel()->getPlugin()->onUploadComplete( arg->getId(), arg->getResultCode() );
}
#endif

void saveCallback( AsyncInvokeArg* arg )
{
    arg->getPluginModel()->getPlugin()->onSaveComplete( arg->getId(), arg->getResultCode() );
}

void scanCallback( AsyncInvokeArg* arg )
{
    arg->getPluginModel()->getPlugin()->onScanComplete( arg->getResult() );
}


#ifdef XP_WIN
static LRESULT CALLBACK PluginWndProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam )
{
    _Plugin* p = ( _Plugin * ) ::GetWindowLongW( hWnd, GWLP_USERDATA );
    PluginModel* model = p->m_model;

    switch ( msg )
    {
    case WM_ERASEBKGND:
        {
            if ( model->hasImage() )
                return 1;
        }
        break;

    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = ::BeginPaint( hWnd, &ps );

            model->render( hdc );

            ::EndPaint( hWnd, &ps );
            return 0;
        }
        break;

    case WM_SIZE:
        {
            uint_t height = HIWORD( lParam );
            uint_t width = LOWORD( lParam );

            model->resize( width, height );

            return 0;
        }
        break;

    case WM_KEYDOWN:
        {
            switch ( wParam )
            {
            case VK_SPACE:
                if ( model->keyDown( wParam ) )
                {
                    ::PostMessage( p->m_hWnd, WM_SETCURSOR, NULL, NULL );
                }
                break;
            }
        }
        break;

    case WM_KEYUP:
        {
            switch ( wParam )
            {
            case VK_SPACE:
                if ( model->keyUp( wParam ) )
                {
                    ::PostMessage( p->m_hWnd, WM_SETCURSOR, NULL, NULL );
                }
                break;
            }
        }
        break;

    case WM_LBUTTONDOWN:
        {
            short xpos = LOWORD( lParam );
            short ypos = HIWORD( lParam );

            model->mouseLeftDown( xpos, ypos );
        }
        break;

    case WM_LBUTTONUP:
        {
            short xpos = LOWORD( lParam );
            short ypos = HIWORD( lParam );

            model->mouseLeftUp( xpos, ypos );
        }
        break;

    case WM_MOUSEMOVE:
        {
            short xpos = LOWORD( lParam );
            short ypos = HIWORD( lParam );

            model->mouseMove( xpos, ypos );
        }
        break;

    case WM_SETCURSOR:
        {
            p->setCursors();
        }
        break;

    case WM_DEVICECHANGE:
        {
            bool connected = wParam == DBT_DEVICEARRIVAL;
            bool disconnected = wParam == DBT_DEVICEREMOVECOMPLETE;

            if ( connected || disconnected )
            {
                ImageAcquisitionModel* ia = model->getIaModel();
                if ( ia != NULL )
                {
                    //::Sleep( 1000 );
                    ia->deviceChange( connected );
                }
            }
        }
        break;

    case WM_CUSTOM_ASYNCCALLBACK:
        {
            AsyncInvoke* asyncInvoke = ( AsyncInvoke* ) lParam;
            AsyncCallback asyncCallback = asyncInvoke->getCallback();
            asyncCallback( asyncInvoke->getArg() );

            //cleanup
            delete asyncInvoke;
        }
        break;

    default:
        break;
    }

    return ::DefWindowProcW( hWnd, msg, wParam, lParam );
}
#endif


void _Plugin::updateView()
{
#ifdef XP_WIN
    //SendMessage( m_hWnd, WM_PAINT, NULL, NULL );
    ::InvalidateRect( m_hWnd, NULL, true );
    ::UpdateWindow( m_hWnd );
#endif
}

char* _Plugin::getUrl()
{
    // get the window object
    //::NPN_GetValue( m_pNPInstance, NPNVWindowNPObject, &m_npoWindow );

    // create a "location" identifier
    NPIdentifier identifier = npid.location;

    // declare a local variant value
    NPVariant variantValue;
    VOID_TO_NPVARIANT( variantValue );

    // get the location property from the window object (which is another object)
    bool b1 = ::NPN_GetProperty( m_npp, m_npoWindow, identifier, &variantValue );

    // get a pointer to the "location" object
    NPObject *locationObj = variantValue.value.objectValue;

    // create a "href" identifier
    identifier = npid.href;

    // get the location property from the location object
    bool b2 = ::NPN_GetProperty( m_npp, locationObj, identifier, &variantValue );

    char* url = NULL;
    String::Copy( &url, variantValue.value.stringValue.UTF8Characters );

    ::NPN_ReleaseVariantValue( &variantValue );

    return url;
}

void _Plugin::setFocus()
{
    ::SetFocus( m_hWnd );
}

void _Plugin::invokeEvent( NPObject* npoEvent )
{
    if ( m_scriptableObject && npoEvent )
    {
        NPVariant result;

        if( !::NPN_InvokeDefault( m_scriptableObject->m_npp, npoEvent, NULL, 0, &result ) )
        {
            this->onError( ResultCode::GeneralFailure );
        }

        ::NPN_ReleaseVariantValue( &result );
    }
}

void _Plugin::invokeEvent( NPObject* npoEvent, int arg0 )
{
    if ( m_scriptableObject && npoEvent )
    {
        NPVariant result;

        NPVariant* npvParam = ( NPVariant* ) ::NPN_MemAlloc( sizeof( NPVariant ) );
        INT32_TO_NPVARIANT( arg0, *npvParam );

        if( !::NPN_InvokeDefault( m_scriptableObject->m_npp, npoEvent, npvParam, 1, &result ) )
        {
            this->onError( ResultCode::GeneralFailure );
        }
        ::NPN_ReleaseVariantValue( &result );
    }
}

void _Plugin::invokeEvent( NPObject* npoEvent, int arg0, int arg1 )
{
    int args[] = { arg0, arg1 };

    invokeEvent( npoEvent, args, 2 );
}

void _Plugin::invokeEvent( NPObject* npoEvent, int args[], int argCount )
{
    if ( m_scriptableObject && npoEvent )
    {
        NPVariant result;

        NPVariant* npvArgs = ( NPVariant* ) ::NPN_MemAlloc( sizeof( NPVariant ) * argCount );
        for ( int i = 0; i < argCount; i++ )
        {
            INT32_TO_NPVARIANT( args[ i ], npvArgs[ i ] );
        }

        if( !::NPN_InvokeDefault( m_scriptableObject->m_npp, npoEvent, npvArgs, argCount, &result ) )
        {
            this->onError( ResultCode::GeneralFailure );
        }
        ::NPN_ReleaseVariantValue( &result );
    }
}

void _Plugin::onScanComplete( int numberOfPages )
{
    invokeEvent( m_scriptableObject->m_npoOnScanComplete, numberOfPages );
}

void _Plugin::onError( ResultCode errorCode )
{
    if ( m_scriptableObject && m_scriptableObject->m_npoOnError )
    {
        NPVariant npvResult;

        NPVariant* npvErrorCode = ( NPVariant* ) ::NPN_MemAlloc( sizeof( NPVariant ) );
        INT32_TO_NPVARIANT( ( int ) errorCode, *npvErrorCode );

        if( !::NPN_InvokeDefault( m_scriptableObject->m_npp, m_scriptableObject->m_npoOnError, npvErrorCode, 1, &npvResult ) )
        {
            throw "Could not invoke scripting method!";
        }
    }
}

void _Plugin::onSelectedImageChanged( int index )
{
    invokeEvent( m_scriptableObject->m_npoOnSelectedImageChanged, index );
}

#ifndef _ADDON
void _Plugin::onUploadComplete( int uploadId, ResultCode resultCode )
{
    invokeEvent( m_scriptableObject->m_npoOnUploadComplete, uploadId, ( int ) resultCode );
}
#endif

void _Plugin::onSaveComplete( int saveId, ResultCode resultCode )
{
    invokeEvent( m_scriptableObject->m_npoOnSaveComplete, saveId, ( int ) resultCode );
}

#ifndef _ADDON
//void _Plugin::onUploadFailed( int uploadId, int resultCode )
//{
//	invokeEvent( m_pScriptableObject->m_npoOnUploadFailed, uploadId, resultCode );
//}
#endif

void _Plugin::onImageCountChanged( int count )
{
    invokeEvent( m_scriptableObject->m_npoOnImageCountChanged, count );
}

void _Plugin::onImagePropertiesChanged( int index )
{
    //if ( m_pScriptableObject )
    //	m_pScriptableObject->m_imageRefreshFlags[ index ] = true;
}

void _Plugin::onImageAcquired( int index )
{
    invokeEvent( m_scriptableObject->m_npoOnImageAcquired, index );
}

void _Plugin::onDeviceEvent(int event)
{
    invokeEvent( m_scriptableObject->m_npoOnDeviceEvent, event );
}

void _Plugin::onZoomChanged()
{
    invokeEvent( m_scriptableObject->m_npoOnZoomChange );
}

void _Plugin::onToolStateChanged( int id, bool state )
{
    invokeEvent( m_scriptableObject->m_npoOnToolStateChange, id, state ? 1 : 0 );
}
